/*
 * By Kristina Striegnitz. Building on some code by Alexander Koller.
 * 
 * The general strategy for this simple NLG system is as follows: It
 * follows the plan returned by the planner step by step (i.e., it
 * instructs the player to move from region to region). If there are
 * any objects associated with the next region, we tell the player to
 * go toward that object (and give some information on where the
 * object is located with respect to the player's current position and
 * orientation). If there are no objects associated with that region,
 * we calculate the angle between the players current orientation and
 * the vector from the player's position to the middle of the region
 * and then give instructions on how to turn in order to be heading in
 * the right direction.
 *
 */

package edu.union.give2.simplenlgservernolandmarks;

import give.comm.ClientMetadata;
import give.formula.Atom;
import give.formula.Formula;
import give.formula.Orientation;
import give.formula.Position;
import give.nlgserver.NlgServerException;
import give.nlgserver.NlgServerInstance;
import give.nlgserver.NlgSystem;
import give.nlgserver.DelayedSendingThread;
import give.nlgserver.discretize.Discretizer;
import give.nlgserver.discretize.RegionDiscretizer;
import give.questionnaire.QuestionnaireData;
import give.world.World;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.lang.Math;

public class ExampleNlgSystem extends NlgSystem {

    private DelayedSendingThread sendToClient;
    private REG realizer;

    private List<Atom> currentPlan;
    private List<String> visibleObjects;

    private String previousPosition;
    private Position playerPosition;
    private Orientation playerOrientation;

    private static final int STRAIGHT = 1;
    private static final int AROUND = 2;
    private static final int LEFT = 3;
    private static final int RIGHT = 4;
    private static final int SLIGHTLYLEFT = 5;
    private static final int SLIGHTLYRIGHT = 6;
    private static final int LEFTAROUND = 7;
    private static final int RIGHTAROUND = 8;
    private static final int DONTKNOW = 0;


    // You MUST have a constructor like this, otherwise the NLG system won't
    // be loaded by the NLG server.
    public ExampleNlgSystem(NlgServerInstance inst, World world, Discretizer discretizer) {
        super(inst, world, discretizer);

        currentPlan = new ArrayList<Atom>();
	visibleObjects = new ArrayList<String>();
    }

    @Override
	public void connectionEstablished(QuestionnaireData preQuestionnaire, ClientMetadata clientMetadata) throws NlgServerException {
	sendToClient = new DelayedSendingThread(this);
	sendToClient.start();
	realizer = new REG(getWorld(), getDiscretizer());


	sendToClient.enqueue("Hello! I am making a plan for you. This may take a while, so please wait.",1800);
	sendToClient.enqueue("Please don't walk around. Or I might get confused.",3000);
	sendToClient.enqueue("But feel free to turn around to get a view of the room.",3000);
	sendToClient.enqueue("Still working... Sorry! But now I should be done soon.",3000);
        replan();

	sendToClient.enqueue("Done! Are you ready?", 0);
	waitfor(1000);
	if (previousPosition==null) {
	    previousPosition = getDiscretizer().getPositionName(getWorld().getPlayerPosition());	    
	}
        sendToClient.enqueue(getNextInstruction(),0); 
    }

    @Override
    public void connectionDisconnected() {
	sendToClient.shutdown();
    }

    private int onExpectedPath(String playerPosName) {

	int steps = 0;
	Atom nextExpectedAction = currentPlan.get(steps);
	while ( nextExpectedAction.getPredicate().equals("move") ) {
	    steps += 1;
	    if ( nextExpectedAction.getArgumentAsString(1).equals(playerPosName) ||
		 getDiscretizer().isNear(nextExpectedAction.getArgumentAsString(1), playerPosName)) {

		return steps;
	    }
	    else {
		nextExpectedAction = currentPlan.get(steps);
	    }
	}
	return 0;
    }


    @Override
    public void handleStatusInformation(Position playerPosition, Orientation playerOrientation, List<String> visibleObjects) throws NlgServerException {

	this.visibleObjects = visibleObjects;
	this.playerPosition = playerPosition;
	this.playerOrientation = playerOrientation;

        if (!currentPlan.isEmpty()) {
            String playerPosName = getDiscretizer().getPositionName(playerPosition);
            Atom expectedAction = currentPlan.get(0);

            if ( ! playerPosName.equals(previousPosition) && ! getDiscretizer().isNear(previousPosition,playerPosName) ) {
                boolean correct = false;
                previousPosition = playerPosName;

                if (expectedAction.getPredicate().equals("move")) {
		    int stepsAccomplished = onExpectedPath(playerPosName);
		    if (stepsAccomplished > 0) {
                        handleCorrectAction(stepsAccomplished);
                        correct = true;
                    }
                }

                if (!correct) {
                    handleIncorrectAction("pos " + playerPosName);
                }

            }

        }
    }

    @Override
    public void handleAction(Atom actionInstance, List<Formula> updates) throws NlgServerException {
        if (!currentPlan.isEmpty()) {
            Atom expectedAction = currentPlan.get(0);
            boolean correctAction = false;

            if (expectedAction.getPredicate().startsWith("manipulate") && expectedAction.getPredicate().equals(actionInstance.getPredicate())) {
                correctAction = expectedAction.getArgumentAsString(0).equals(actionInstance.getArgumentAsString(0));
            }

            if (expectedAction.getPredicate().startsWith("take")) {
                correctAction = expectedAction.getPredicate().equals(actionInstance.getPredicate());
            }

            if (correctAction) {
                handleCorrectAction(1);
            } else {
                handleIncorrectAction(actionInstance.toString());
            }
        }
    }

    private void handleIncorrectAction(String error) throws NlgServerException {
	sendToClient.clear();
        sendToClient.enqueue("Wait, that's not what I wanted you to do. I need to make a new plan.", 1500);

        replan();

        sendToClient.enqueue(getNextInstruction(), 0);
    }

    private void handleCorrectAction(int stepsAccomplished) throws NlgServerException {
	for (int i = 1; i<=stepsAccomplished; i++) {
	    currentPlan.remove(0);
	}

        sendToClient.enqueue("Excellent! "+getNextInstruction(), 0);
    }

    @Override
    public void handleDidNotUnderstand() throws NlgServerException {
        sendToClient.enqueue("I said: " + getNextInstruction(), 0);
    }

    @Override
    public void handleExternalEvent(String sourceName, Map<String, String> data)  {
        // ignore external events
    }

    private void replan() throws NlgServerException {
	sendToClient.enqueue("Please don't walk around while I am planning.", 1500);
	sendToClient.enqueue("Turning is ok though. If you want to have a look at the room.", 3000);
	sendToClient.enqueue("I am almost done now.", 0);
        try {
            currentPlan = getPlan(getWorld().getGoals());
        } catch (IOException e) {
            throw new NlgServerException(e);
        }
	sendToClient.clear();
    }

    private String getNextInstruction() throws NlgServerException {
        if( currentPlan == null ) {
	    sendToClient.enqueue("Oops. Something seems to have gone wrong. I need to make a new plan.",1500);
            replan();
	    
            if( currentPlan == null ) {
                return "Sorry, I am confused...@@@@";
            }
        }
        
        if (currentPlan.isEmpty()) {
            return null;
        } else {
	    String message = verbalizePlanStep(currentPlan.get(0));  //currentPlan.get(0).toString();
	    return message;
        }

    }

    private void waitfor(int ms) {
        try {
            Thread.sleep(ms);
        } catch (InterruptedException e) {
        }
    }


    private String verbalizePlanStep(Atom planStep)
    {
	String pred = planStep.getPredicate();

	/* deal with move actions */
	if (pred.startsWith("move")) {
	    String from = planStep.getArgumentAsString(0);
	    String to = planStep.getArgumentAsString(1);

	    RegionDiscretizer.RegionInfo to_reg_info = ((RegionDiscretizer)getDiscretizer()).getRegionInformation(to);
	    RegionDiscretizer.RegionInfo from_reg_info = ((RegionDiscretizer)getDiscretizer()).getRegionInformation(from);

	    if ( playerPosition == null ) {
		playerPosition = getWorld().getPlayerPosition(); //getDiscretizer().getApproximatePosition(from);
		playerOrientation = getWorld().getPlayerOrientation(); //new Orientation(-1.0, 0.0, 0.0);
	    }

	    /* If we are leaving the room ...*/
	    if ( ! to_reg_info.getRoom().equals(from_reg_info.getRoom())) {
		/* Note: I am not checking whether the player is actually facing the door or 
		   whether the door is visible. I am also not dealing with potentially
		   multiple doors to different rooms in the same region. */
		return("Go through the door.");
	    }

	    /* staying in the same room */
	    else {
		if ( ! to_reg_info.getNearObjects().isEmpty()) {
		    /* Pick an object that is both visible and in the "to" region 
		       and tell player to go toward that object. If there is no such object,
		       randomly pick one of the objects in the region and tell player to
		       turn until object becomes visible. Special treatment for doors.*/

		    List<String> visible_and_near = intersection(visibleObjects, to_reg_info.getNearObjects());
		    
		    if ( ! visible_and_near.isEmpty()) {
			String target = visible_and_near.get(0);
			String msg = "Go to the " + realizer.makeReferringExpression(target, visibleObjects) + ". It should be ";
			int dir = getHeading(getWorld().getPosition(target), playerPosition, playerOrientation);
			switch (dir) {
			case STRAIGHT: msg += "straight in front of you."; break;
			case AROUND: msg += "behind you."; break;
			case LEFT: msg += "on your left."; break;
			case RIGHT: msg += "on your right."; break;
			case SLIGHTLYLEFT: msg += "in front of you slightly to the left."; break;
			case SLIGHTLYRIGHT: msg += "in front of you slightly to the right."; break;
			case LEFTAROUND: msg += "on the left behind you."; break;
			case RIGHTAROUND: msg += "on the right behind you."; break;
			default: msg += "somewhere in this room."; break;
			}
			return msg;
		    }
		    else {

			String door = checkForDoor(to_reg_info.getNearObjects());

			if ( door != null ) {
			    int dir = getHeading(getDiscretizer().getApproximatePosition(to), playerPosition, playerOrientation);
			    String msg = "";
			    switch (dir) {
			    case STRAIGHT: msg = "Go straight"; break;
			    case AROUND: msg = "Turn around, then go"; break;
			    case LEFT: msg = "Go left"; break;
			    case RIGHT: msg = "Go right"; break;
			    case SLIGHTLYLEFT: msg = "Go slightly left"; break;
			    case SLIGHTLYRIGHT: msg = "Go slightly right"; break;
			    case LEFTAROUND: msg = "Turn left, almost all the way around but not quite. Then go"; break;
			    case RIGHTAROUND: msg = "Turn right, almost all the way around but not quite. Then go"; break;
			    default: msg = "Go"; break;
			    }
			    return msg + " toward the door.";
			}
			else {
			    String target = to_reg_info.getNearObjects().get(0);
			    String msg = "Look around until you see a " + realizer.makeReferringExpression(target, getWorld().getRoomContents(from_reg_info.getRoom()))+". Then go toward it. It should be ";

			    int dir = getHeading(getWorld().getPosition(target), playerPosition, playerOrientation);
			    switch (dir) {
			    case STRAIGHT: msg += "straight in front of you."; break;
			    case AROUND: msg += "behind you."; break;
			    case LEFT: msg += "on your left."; break;
			    case RIGHT: msg += "on your right."; break;
			    case SLIGHTLYLEFT: msg += "in front of you slightly to the left."; break;
			    case SLIGHTLYRIGHT: msg += "in front of you slightly to the right."; break;
			    case LEFTAROUND: msg += "on the left behind you."; break;
			    case RIGHTAROUND: msg += "on the right behind you."; break;
			    default: msg += "somewhere in this room."; break;
			    }
			    return msg;
			    
			}
		    }

		}
		else {
		    /* If there are no objects in the "to" region, give instructions to go to the center
		       of the next region. */

		    int dir = getHeading(getDiscretizer().getApproximatePosition(to), playerPosition, playerOrientation);
		    String msg = "";
		    switch (dir) {
		    case STRAIGHT: msg = "Go straight."; break;
		    case AROUND: msg = "Turn around and then go forward."; break;
		    case LEFT: msg = "Go to the left."; break;
		    case RIGHT: msg = "Go to the right."; break;
		    case SLIGHTLYLEFT: msg = "Go slightly left."; break;
		    case SLIGHTLYRIGHT: msg = "Go slightly right."; break;
		    case LEFTAROUND: msg = "Turn left, almost all the way around but not quite. Then go in that direction."; break;
		    case RIGHTAROUND: msg = "Turn right, almost all the way around but not quite. Then go in that direction."; break;
		    default:  msg = "Go toward the middle of the room."; break;
		    }
		    return msg;

		}
	    }
	}

	/* manipulate actions */
	else if (pred.startsWith("manipulate")) {
	    String what = planStep.getArgumentAsString(0);
	    return("Press the "+realizer.makeReferringExpression(what, visibleObjects)+".");
	}

	/* take actions */
	else if (pred.startsWith("take-t1")) {
	    return("Take the trophy.");
	}

	/* everything else: just print as is */
	else {
	    return planStep.toString();
	}
    }


    private String checkForDoor (List<String> objects) {

	for ( String o : objects ) {
	    String type = getWorld().getType(o);
	    if (type != null && type.equals("door")) {
		return o;
	    }
	}
	return null;
    }

    private int getHeading (Position targetPosition, Position playerPosition, Orientation playerOrientation)
    {

	double angle = Math.toDegrees(Math.atan2(targetPosition.getZ()-playerPosition.getZ(), targetPosition.getX()-playerPosition.getX()) - Math.atan2(playerOrientation.getZ(), playerOrientation.getX()));

	if ( angle < -180.0 ) {
	    angle = 360.0 + angle;
	}
	else if ( angle > 180.0 ) {
	    angle = -360.0 + angle;
	}

	if ( angle >= -20.0 && angle <= 20.0 ) {
	    return STRAIGHT;
	}
	else if ( angle <= -160.0 || angle >= 160.0 ) {
	    return AROUND;
	}
	else if ( angle >= 60.0 && angle <= 120.0 ) {
	    return RIGHT;
	}
	else if ( angle <= -60.0 && angle >=-120 ) {
	    return LEFT;
	}
	else if ( angle < 60.0 && angle > 20.0 ) {
	    return SLIGHTLYRIGHT;
	}
	else if ( angle > -60.0 && angle < -20.0 ) {
	    return SLIGHTLYLEFT;
	}
	else if ( angle > 120.0 && angle < 160.0 ) {
	    return RIGHTAROUND;
	}
	else if ( angle < -120.0 && angle > -160.0 ) {
	    return LEFTAROUND;
	}
	else {
	    return DONTKNOW;
	}

    }


    private List<String> intersection (List<String> list1, List<String> list2) {
	
	List<String> result = new ArrayList();

	for (String element : list1) {
	    if (list2.contains(element)) {
		result.add(element);
	    }
	}
	return result;
    }


}
